﻿using System.Linq;
using System.Text;
using System.Data;
using System.Xml;
using System;
using System.Collections.Generic;

namespace Orion.CAMLLib
{
    /// <summary>
    /// This class is used for generating CAML Query for fields.
    /// Usage :
    /// </summary>
    /// <example>
    /// Orion.CAMLLib.CAMLGenerator cGen = new Orion.CAMLLib.CAMLGenerator();
    /// cGen.AddWhereField("Title", "Jigar", "Text", "Eq", String.Empty);
    /// cGen.AddWhereField("Title", "Nitin", "Text", "Eq", "Or");
    /// cGen.AddWhereField("Password", "Pass123", "Text", "Eq", "And");
    /// cGen.AddWhereField("ID", "1", "Text", "Eq", "And");
    /// cGen.AddWhereField("Created", DateTime.Now.ToString("yyyy-MM-ddThh:mm:ssZ"), "DateTime", "Leq", "And");
    /// textBox1.Text = cGen.CamlQuery;
    /// </example>
    public class CAMLGenerator
    {
        #region Private Members

        private XmlDocument _camlDocument = null;
        private XmlElement _queryNode = null;
        private XmlElement _whereNode = null;
        private XmlElement _filterNode = null;
        private XmlElement _lastNode = null;
        private StringBuilder output = new StringBuilder();

        private string _lastOperator = String.Empty;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the generated caml query.
        /// </summary>
        /// <value>The caml query.</value>
        public string CamlQuery
        {
            get
            {
                return _camlDocument.InnerXml;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Initializes a new instance of the <see cref="CAMLGenerator"/> class.
        /// </summary>
        public CAMLGenerator()
        {
            _camlDocument = new XmlDocument();
            _queryNode = _camlDocument.CreateElement("Query");
        }

        /// <summary>
        /// Adds the where field.
        /// </summary>
        /// <param name="InternalName">Name of the internal.</param>
        /// <param name="Value">The value.</param>
        /// <param name="DataType">Type of the data.</param>
        /// <param name="Operator">The operator.</param>
        /// <param name="Filter">The filter.</param>
        public void AddWhereField(string InternalName, string Value, string DataType, string Operator, string Filter)
        {
            XmlNode xmlWhereNode = _queryNode.SelectSingleNode("//Where");

            // Generate the Where Clause element
            if (xmlWhereNode == null)
            {
                xmlWhereNode = _camlDocument.CreateElement("Where");
                _queryNode.AppendChild(xmlWhereNode);
                _whereNode = (XmlElement)xmlWhereNode;
                _camlDocument.AppendChild(_queryNode);
                SaveDocument();
            }

            // If the field is the very first field then simply create the field and append to Where Element.
            if (String.IsNullOrEmpty(Filter))
            {
                _filterNode = _camlDocument.CreateElement(Operator);
                _filterNode.InnerXml = CreateField(InternalName, DataType, Value);
                _lastNode = _filterNode;
                _whereNode.InnerXml = _filterNode.OuterXml;
                SaveDocument();
            }
            else
            {
                // Find the last filter Node. i.e. And, Or etc.
                XmlNode nodeFilter = FindNode(_filterNode.Name, _filterNode.InnerText);

                // After creating very first element, if it is the second element then nodeFilter and last node will be same.
                // So we are putting the check here. If they are not equal then we need to remove the last inserted node from its parent
                // and append it to newly created operator.

                // If both are equal then we need to simply append the field xml to the where clause.
                if (nodeFilter.OuterXml != _lastNode.OuterXml)
                {
                    XmlElement newFilterNode = _camlDocument.CreateElement(Filter);

                    IEnumerable<XmlElement> lastNodes = from XmlElement node in nodeFilter.ChildNodes
                                                        where node.OuterXml == _lastNode.OuterXml
                                                        select node;

                    nodeFilter.RemoveChild((XmlElement)lastNodes.First());

                    newFilterNode.AppendChild(_lastNode);

                    XmlElement oNode = _camlDocument.CreateElement(Operator);
                    oNode.InnerXml = CreateField(InternalName, DataType, Value);
                    newFilterNode.AppendChild(oNode);
                    nodeFilter.AppendChild(newFilterNode);
                    SaveDocument();
                    _lastNode = oNode;
                    _filterNode = newFilterNode;
                }
                else
                {
                    string opNodeXML = _filterNode.OuterXml;
                    _filterNode = null;
                    _filterNode = _camlDocument.CreateElement(Filter);
                    XmlElement oNode = _camlDocument.CreateElement(Operator);
                    oNode.InnerXml = CreateField(InternalName, DataType, Value);
                    _filterNode.InnerXml = opNodeXML + oNode.OuterXml;
                    _whereNode.InnerXml = _filterNode.OuterXml;
                    _lastNode = oNode;
                    SaveDocument();
                }
            }
            _lastOperator = Operator;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Creates the field.
        /// </summary>
        /// <param name="FieldName">Name of the field.</param>
        /// <param name="ValueType">Type of the value.</param>
        /// <param name="Value">The value.</param>
        /// <returns></returns>
        private string CreateField(string FieldName, string ValueType, string Value)
        {
            StringBuilder fieldRef = new StringBuilder();
            fieldRef.Append("<FieldRef Name='" + FieldName + "' />");
            fieldRef.Append("<Value Type=" + "'" + ValueType + "'" + " >");
            fieldRef.Append(Value);
            fieldRef.Append("</Value>");
            return fieldRef.ToString();
        }

        /// <summary>
        /// Finds the node.
        /// </summary>
        /// <param name="NodeName">Name of the node.</param>
        /// <param name="InnerText">The inner text.</param>
        /// <returns></returns>
        private XmlNode FindNode(string NodeName, string InnerText)
        {
            XmlNode nodeToFind = null;
            foreach (XmlNode node in _camlDocument.DocumentElement.ChildNodes)
            {
                XmlNode foundNode = ProcesNode(node, _camlDocument.DocumentElement.Name, NodeName, InnerText);
                if (foundNode != null)
                {
                    nodeToFind = foundNode;
                    break;
                }
            }
            return nodeToFind;
        }

        /// <summary>
        /// Proceses the node.
        /// </summary>
        /// <param name="node">The node.</param>
        /// <param name="parentPath">The parent path.</param>
        /// <param name="NodeName">Name of the node.</param>
        /// <param name="InnerText">The inner text.</param>
        /// <returns></returns>
        private XmlNode ProcesNode(XmlNode node, string parentPath, string NodeName, string InnerText)
        {
            XmlNode nodeToReturn = null;
            if (node.Name == NodeName && node.InnerText == InnerText)
            {
                nodeToReturn = node;
            }
            else
            {
                foreach (XmlNode child in node.ChildNodes)
                {
                    XmlNode node1 = ProcesNode(child, parentPath + "/" + node.Name, NodeName, InnerText);
                    if (node1 != null)
                        nodeToReturn = node1;
                }
            }
            return nodeToReturn;
        }

        /// <summary>
        /// Saves the XML document.
        /// </summary>
        private void SaveDocument()
        {
            if (output.Length > 0)
                output.Remove(0, output.Length - 1);

            XmlWriter xt = XmlWriter.Create(output);
            _camlDocument.Save(xt);
        }

        #endregion
    }
}
